package com.hww.common.util;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MD5Utils {
    private static final String MD5_REGEX = "^[a-fA-F0-9]{32}$";
    private static final String HEX_REGEX = "^([a-fA-F0-9]{2})+$";


    /**
     * 生成源字节数据的MD5加密摘要。
     * <p>
     * 使用MD5算法对输入的字节数据进行加密，返回加密后的摘要字节数据。
     * 如果输入的源数据为null，则直接返回null。
     * <p>
     * 注意：此方法捕获并抛出了NoSuchAlgorithmException，这意味着如果系统不支持MD5算法，
     * 将会抛出一个运行时异常。
     *
     * @param source 源字节数据，用于生成MD5摘要
     * @return MD5加密后的摘要字节数据，如果源数据为null则返回null
     */
    public static final byte[] getMD5(byte[] source) {
        if (null == source) {
            return null;
        }
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            return md.digest(source);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 从ByteBuffer中获取所有未读的字节，不改变Buffer的位置。
     * <p>
     * 此方法创建一个新的字节数组，长度为Buffer的剩余字节个数，并将这些字节从Buffer中复制到数组中。
     * 完成后，Buffer的位置保持不变，允许后续操作继续从当前位置开始。
     *
     * @param buffer 提供字节数据的ByteBuffer。如果为null，则返回null。
     * @return 一个包含Buffer中所有未读字节的字节数组，或者如果输入为null，则返回null。
     */
    public static final byte[] getBytesInBuffer(ByteBuffer buffer) {
        if (null == buffer) {
            return null;
        }
        int pos = buffer.position();
        try {
            byte[] bytes = new byte[buffer.remaining()];
            buffer.get(bytes);
            return bytes;
        } finally {
            buffer.position(pos);
        }
    }

    /**
     * 计算给定ByteBuffer对象的MD5校验码。
     * <p>
     * 该方法通过首先将ByteBuffer对象转换为字节数组，然后计算该字节数组的MD5值。
     * 如果输入的ByteBuffer为null，则返回null。否则，返回包含计算得到的MD5值的ByteBuffer对象。
     *
     * @param source 待计算MD5校验码的ByteBuffer对象。
     * @return 计算得到的MD5校验码的ByteBuffer对象，如果输入为null则返回null。
     * @see #getMD5(byte[])
     */
    public static final ByteBuffer getMD5(ByteBuffer source) {
        return null == source ? null : ByteBuffer.wrap(getMD5(getBytesInBuffer(source)));
    }

    /**
     * 将字节数组转换为十六进制字符串。
     * 此方法用于将二进制数据表示为更易读的十六进制字符串形式。每个字节被转换为两个十六进制字符。
     *
     * @param buffer 待转换的字节数组。如果为null，则返回null。
     * @return 转换后的十六进制字符串。如果输入为null，则返回null。
     */
    public static final String toHex(byte[] buffer) {
        if (null == buffer) {
            return null;
        }
        StringBuffer sb = new StringBuffer(buffer.length * 2);
        for (int i = 0; i < buffer.length; i++) {
            /*获取当前字节的高4位，并转换为十六进制字符*/
            sb.append(Character.forDigit((buffer[i] & 240) >> 4, 16));
            /*获取当前字节的低4位，并转换为十六进制字符*/
            sb.append(Character.forDigit(buffer[i] & 15, 16));
        }
        return sb.toString();
    }

    /**
     * @see #toHex(byte[])
     */
    public static final String toHex(ByteBuffer buffer) {
        return toHex(getBytesInBuffer(buffer));
    }

    /**
     * 字符串验证器,根据正则表达式判断字符串是否为十六进制(HEX)字符串
     * 输入为null或空或正则表达式不匹配则返回false
     */
    public static final boolean validHEX(String input) {
        return input != null && input.matches(HEX_REGEX);
    }


    /**
     * 将十六进制字符串转换为字节数组。
     * 每两个字符组成一个字节，大写和小写字母均被识别。
     *
     * @param src 十六进制字符串。如果为null，则返回null。
     * @return 由十六进制字符串表示的字节数组，如果输入为null，则返回null。
     */
    public static final byte[] hex2Bytes(String src) {
        if (null == src) {
            return null;
        }
        byte[] res = new byte[src.length() / 2];
        char[] chs = src.toCharArray();
        int[] b = new int[2];

        /*遍历字符数组，每次处理两个字符*/
        for (int i = 0, c = 0; i < chs.length; i += 2, c++) {
            for (int j = 0; j < 2; j++) {
                if (chs[i + j] >= '0' && chs[i + j] <= '9') {
                    b[j] = (chs[i + j] - '0');
                } else if (chs[i + j] >= 'A' && chs[i + j] <= 'F') {
                    b[j] = (chs[i + j] - 'A' + 10);
                } else if (chs[i + j] >= 'a' && chs[i + j] <= 'f') {
                    b[j] = (chs[i + j] - 'a' + 10);
                }
            }
            /*将高4位和低4位组合成一个字节*/
            b[0] = (b[0] & 0x0f) << 4;
            b[1] = (b[1] & 0x0f);
            res[c] = (byte) (b[0] | b[1]);
        }

        return res;
    }

    /**
     * 将十六进制字符串转换为ByteBuffer。
     * <p>
     * 此方法首先将十六进制字符串转换为字节数组，然后使用这个字节数组创建一个ByteBuffer对象。
     * 如果输入的字符串为null，则直接返回null，不进行转换。
     * 这种转换对于处理二进制数据的十六进制表示非常有用，例如在网络通信或文件处理中。
     *
     * @param src 十六进制字符串，代表要转换的二进制数据。
     * @return 转换后的ByteBuffer对象，如果输入字符串为null，则返回null。
     */
    public static final ByteBuffer hex2ByteBuffer(String src) {
        return null == src ? null : ByteBuffer.wrap(hex2Bytes(src));
    }

    /**
     * 生成源字节数组的MD5哈希值的字符串表示。
     * <p>
     * 此方法通过调用getMD5方法获取MD5哈希值，然后使用toHex方法将哈希值转换为十六进制字符串。
     * 主要用于需要以字符串形式传递或显示MD5哈希值的场景。
     *
     * @param source 源字节数组，将计算其MD5哈希值。
     * @return 返回源字节数组的MD5哈希值的十六进制字符串表示。
     * @see #getMD5(byte[])
     * @see #toHex(byte[])
     */
    public static final String getMD5String(byte[] source) {
        return toHex(getMD5(source));
    }

    /**
     * 计算给定ByteBuffer对象的MD5哈希值并以十六进制字符串形式返回。
     *
     * @param source 待计算MD5值的ByteBuffer对象。
     *               ByteBuffer是Java中用于处理字节序列的类，它可以更有效地处理字节数据。
     * @return 返回计算得到的MD5散列值的十六进制字符串表示。
     * @see #getMD5(byte[])
     * @see #toHex(byte[])
     */
    public static final String getMD5String(ByteBuffer source) {
        return toHex(getMD5(source));
    }

    /**
     * 检查给定的字符串是否为有效的MD5哈希值。
     * <p>
     * MD5（Message-Digest Algorithm 5）是一种广泛使用的加密散列函数，产生一个128位（16字节）的散列值。
     * 通常表示为一个32位的十六进制数。此方法通过匹配一个预定义的正则表达式来验证字符串是否符合MD5哈希值的格式。
     *
     * @param md5 待验证的MD5字符串。
     * @return 如果输入字符串是有效的MD5哈希值，则返回true；否则返回false。
     */
    public static final boolean validMd5(String md5) {
        return null != md5 && md5.matches(MD5_REGEX);
    }

    /**
     * 从{@link InputStream}读取字节数组<br>
     * 当{@code in}为{@link FileInputStream}时，调用{@link #readBytes(FileInputStream)}(NIO方式)读取<br>
     * 结束时会关闭{@link InputStream}
     *
     * @param in 为{@code null}返回{@code null}
     * @return 读取的字节数组
     * @throws IOException
     * @throws IllegalArgumentException {@code in}为{@code null}
     */
    public static final byte[] readBytes(InputStream in) throws IOException, IllegalArgumentException {
        if (null == in) {
            return null;
        }
        if (in instanceof FileInputStream) {
            return readBytes((FileInputStream) in);
        }
        try {
            int buffSize = Math.max(in.available(), 1024 * 8);
            byte[] temp = new byte[buffSize];
            ByteArrayOutputStream out = new ByteArrayOutputStream(buffSize);
            int size = 0;
            while ((size = in.read(temp)) != -1) {
                out.write(temp, 0, size);
            }
            return out.toByteArray();
        } finally {
            in.close();
        }
    }

    /**
     * NIO方式从{@link FileInputStream}读取字节数组<br>
     * 结束时会关闭{@link InputStream}
     *
     * @param fin {@link FileInputStream}
     * @return 返回读取的字节数 当{@code fin}为null时返回null;
     * @throws IOException
     */
    public static final byte[] readBytes(FileInputStream fin) throws IOException {
        if (null == fin) {
            return null;
        }
        FileChannel fc = fin.getChannel();
        try {
            ByteBuffer bb = ByteBuffer.allocate((int) fc.size());
            fc.read(bb);
            bb.flip();
            return bb.array();
        } finally {
            if (null != fc) {
                fc.close();
            }
            fin.close();
        }
    }

    /**
     * NIO方式将{@code data}数据保存在{@code file}指定的文件中<br>
     * 如果{@code file}所在文件夹不存在，则会自动创建所有的文件夹<br>
     *
     * @param data
     * @param file      文件保存的位置
     * @param overwrite 同名文件存在时是否覆盖
     * @return 返回保存的文件名
     * @throws IOException              {@code file}存在但不是文件或其他IO异常
     * @throws IllegalArgumentException {@code data}为null时
     */
    public static final File saveBytes(byte[] data, File file, boolean overwrite) throws IOException,
            IllegalArgumentException {
        if (data == null) {
            throw new IllegalArgumentException("data is null");
        }
        FileOutputStream out = null;
        FileChannel fc = null;
        try {
            File folder = file.getParentFile();
            if (!folder.exists()) {
                folder.mkdirs();
            }
            long free = folder.getFreeSpace() >> 20;//可用磁盘空间(MB)
            if (free < 10) {
                throw new IOException(String.format("DISK ALMOST FULL(磁盘空间不足) FREE %dMB,%s", free, folder.getAbsolutePath()));
            }
            if (!file.exists() || !file.isFile() || overwrite) {
                out = new FileOutputStream(file);
                fc = out.getChannel();
                ByteBuffer bb = ByteBuffer.wrap(data);
                fc.write(bb);
            }
            return file;
        } finally {
            if (null != fc) {
                fc.close();
            }
            if (null != out) {
                out.close();
            }
        }
    }
}